<?php
namespace Lsf\Plugin;

/**
 * 配置中心插件类（基于zookeeper）
 * @author lichenkai@okay.cn
 * $Id: config_center.php 2019-08-05 19:23:10 lichenkai $
 */

class ConfigCenter
{
    const CONFIG_CACHE_FILE_NAME_SEPARATE_SIGN = '-';
    const ZOOKEEPER_GROUP_SEPARATE_SIGN = ':';
    const CUSTOM_GROUP_SEPARATE_SIGN = '@';
    const CUSTOM_DEFAULT_GROUP_NAME = 'default';
    const CONFIG_CACHE_DIR = WEBPATH . '/cache/config_center/';

    private $_zookeeperConfig = [];
    private $_zookeeper;
    private $_currentPath = '';

    /**
     * 构造函数
     * @param  void
     * @return void
     */
    public function __construct(){
        // 初始化zookeeper
        // $this->_zookeeper = \Lsf\Loader::module('zookeeper');
        // 加载zookeeper配置文件
        $zookeeperConfig = \Lsf\Loader::config('zookeeper', TRUE, 2);
        if(empty($zookeeperConfig)){
            throw new \Exception('zookeeper config is empty');
        }else{
            // 检查zookeeper路径
            if(!isset($zookeeperConfig['path']) || empty($zookeeperConfig['path'])){
                throw new \Exception('zookeeper config path invalid');
            }
            if(!isset($zookeeperConfig['version']) || empty($zookeeperConfig['version'])){
                throw new \Exception('zookeeper config version invalid');
            }
            $this->_currentPath = $zookeeperConfig['path'] . $zookeeperConfig['version'];
            $this->_zookeeperConfig = $zookeeperConfig;
        }
        // 加载配置中心配置文件
        // $this->_configCenterConfig = \Lsf\Loader::config('config_center', TRUE);
        // if(empty($this->_configCenterConfig)){
        //     throw new \Exception('config_center config is empty');
        // }else{
        //     if(!isset($this->_configCenterConfig['cache_path']) || empty($this->_configCenterConfig['cache_path'])){
        //         throw new \Exception('config_center config cache_path invalid');
        //     }
        //     // 检查本地缓存文件的可用性
        //     if(is_dir($this->_configCenterConfig['cache_path'])){
        //         if(!is_writeable($this->_configCenterConfig['cache_path']) || !chmod($this->_configCenterConfig['cache_path'], 0755)){
        //             throw new \Exception('config cache path ' . $this->_configCenterConfig['cache_path'] . ' unwriteable');
        //         }
        //     }
        // }
        // 检查本地缓存目录的可用性
        if(is_dir(self::CONFIG_CACHE_DIR)){
            if(!is_writeable(self::CONFIG_CACHE_DIR) || !chmod(self::CONFIG_CACHE_DIR, 0755)){
                throw new \Exception('config cache path ' . self::CONFIG_CACHE_DIR . ' unwriteable');
            }
        }
    }

    /**
     * 配置中心初始化监听
     * [说明]
     * 加载zookeeper远程配置到本地
     * 监听配置变化
     * @param  void
     * @return void
     */
    public function initWatch(){
        // 初始化zookeeper
        $this->_zookeeper = \Lsf\Loader::module('zookeeper');
        $this->getAll(TRUE, TRUE, TRUE);
        // while($watch === TRUE){
        //     sleep(3);
        // }
    }

    /**
     * 获取单个配置
     * [说明]
     * 该方法只支持缓冲可用时使用，不等同于zookeeper的get
     * @param  string  $key
     * @param  string  $customGroup
     * @param  string  $zookeeperGroup
     * @return mixed
     */
    public function get($key, $customGroup, $zookeeperGroup = 'unchange'){
        $cacheFilePath = $this->_getConfigCacheFilePath();
        // 优先使用缓冲
        if(is_file($cacheFilePath)){
            $cacheConfig = include($cacheFilePath);
            if(isset($cacheConfig[$zookeeperGroup][$customGroup][$key])){
                return $cacheConfig[$zookeeperGroup][$customGroup][$key];
            }
        // 远程加载配置    
        }else{
            $path = $this->_currentPath . '/' . $zookeeperGroup . '/';
            if(!empty($customGroup)){
                $path.= $customGroup . self::CUSTOM_GROUP_SEPARATE_SIGN . $key;
            }else{
                $path.= $key;
            }
            $result = $this->_zookeeper->exists($path);
            // 节点不存在
            if(!$result){
                throw new \Exception('config zookeeper path ' . $path . ' not exists');
            }else{
                $result = $this->_zookeeper->get($path);
                if($result){
                    return $result;
                }
            }
        }
        return FALSE;
    }

    /**
     * 获取节点下所有信息
     * [说明]
     * 该方法可以获取全部配置信息并支持缓存读取
     * 开启cache后当没有缓存或强制刷新缓存会自动生成/更新缓存
     * @param  bool   $cache
     * @param  bool   $forceRefresh
     * @param  bool   $watch
     * @return array
     */
    public function getAll($cache = TRUE, $forceRefresh = FALSE, $watch = FALSE){
        // 开启缓冲
        if($cache === TRUE){
            $cacheFilePath = $this->_getConfigCacheFilePath();
            if($forceRefresh === FALSE && is_file($cacheFilePath)){
                return include_once($cacheFilePath);
            }
        }
        // 递归获取配置
        $config = $this->_recursionGetConfig($this->_currentPath, $watch);
        if($config === FALSE){
            throw new \Exception('config path ' . $this->_currentPath . ' not exists');
        }
        // 开启缓存则自动缓存
        if($cache === TRUE && !empty($config)){
            // 转换为分组键值对
            $groupKVConfig = $this->generateGroupKV($config, 1);
            $this->generateConfigCache($groupKVConfig);
        }
        return $config;
    }

    /**
     * 获取组配置
     * [说明]
     * 该方法同时支持zookeeper标准的group及自定义二级group
     * @param  string  $customGroup     自定义组名
     * @param  string  $zookeeperGroup  zookeeper组名
     */
    public function group($customGroup, $zookeeperGroup = 'unchange'){
        $cacheFilePath = $this->_getConfigCacheFilePath();
        // 优先使用缓冲
        if(is_file($cacheFilePath)){
            $cacheConfig = include($cacheFilePath);
            if(isset($cacheConfig[$zookeeperGroup][$customGroup]) && !empty($cacheConfig[$zookeeperGroup][$customGroup])){
                return $cacheConfig[$zookeeperGroup][$customGroup];
            }
        // 远程加载配置    
        }else{
            $path = $this->_currentPath . '/' . $zookeeperGroup;
            $config = $this->_recursionGetConfig($path, FALSE);
            if($config === FALSE){
                throw new \Exception('config path ' . $path . ' not exists');
            }
            $kvConfig = $this->generateGroupKV($config, 2);
            if(!isset($kvConfig[$customGroup])){
                throw new \Exception('config path ' . $path . ' not exists custom group ' . $customGroup);
            }else{
                return $kvConfig[$customGroup];
            }
        }
        return FALSE;
    }

    /**
     * 监听path变更回调函数
     * @param  int     $eventType
     * @param  int     $stat
     * @param  string  $path
     * @return void
     */
    public function watcher($eventType, $stat, $path){
        $logData = [
            'event' => 'reveive zookeeper path change',
            'stat'  => $stat,
            'path'  => $path
        ];
        \Lsf\Loader::plugin('Log')->info('', $logData, 'config_center');
        $value = $this->_zookeeper->watch($path, [$this, 'watcher']);
        // 变更本地缓冲
        $cacheFilePath = $this->_getConfigCacheFilePath();
        if(is_file($cacheFilePath)){
            $tmpArr = explode($this->_zookeeperConfig['version'], $path);
            if(!isset($tmpArr[1]) || empty($tmpArr[1])){
                $logData = ['error' => 'zookeeper path exception one', 'path' => $path];
                \Lsf\Loader::plugin('Log')->error('', $logData, 'config_center');
            }else{
                $tmpStr = trim($tmpArr[1], '/');
                $tmpArr = explode('/', $tmpStr);
                if(count($tmpArr) < 2){
                    $logData = [
                        'error' => 'zookeeper path exception two',
                        'path' => $path
                    ];
                    \Lsf\Loader::plugin('Log')->error('', $logData, 'config_center');
                }else{
                    $zkGroupName = $tmpArr[0];
                    $customGroupName = self::CUSTOM_DEFAULT_GROUP_NAME;
                    $tmpArr = explode(self::CUSTOM_GROUP_SEPARATE_SIGN, $tmpArr[1]);
                    if(isset($tmpArr[1])){
                        $key = $tmpArr[1];
                        $customGroupName = $tmpArr[0];
                    }else{
                        $key = $tmpArr[0];
                    }
                    $cacheFilePath = $this->_getConfigCacheFilePath();
                    $config = include($cacheFilePath);
                    if(isset($config[$zkGroupName][$customGroupName][$key])){
                        $config[$zkGroupName][$customGroupName][$key] = $value;
                        $this->generateConfigCache($config);
                        $logData = [
                            'update_zk_group_name' => $zkGroupName,
                            'update_custom_group_name' => $customGroupName,
                            'update_key_name' => $key,
                            'new_value' => $value
                        ];
                        \Lsf\Loader::plugin('Log')->info('', $logData, 'config_center');
                    }else{
                        $logData = [
                            'error'             => 'zookeeper path value change but local cache not exist',
                            'zk_group_name'     => $zkGroupName,
                            'custom_group_name' => $customGroupName,
                            'key_name'          => $key,
                            'new_value'         => $value
                        ];
                        \Lsf\Loader::plugin('Log')->error(9000022, $logData, 'config_center');
                    }
                }
            }
        }
    }

    /**
     * 清除配置缓冲文件
     * @param  void
     * @return void
     */
    // public function clearConfigCache(){
    //     $filePath = $this->_getConfigCacheFilePath();
    //     if(is_file($filePath)){
    //         if(!is_writeable($filePath) || !chmod($filePath, 0755)){
    //             throw new \Exception('config cache path ' . $filePath . ' unwriteable');
    //         }else{
    //             $fd = @fopen($filePath, 'w');
    //             if(is_resource($fd)){
    //                 fwrite($fd, '');
    //                 fclose($fd);
    //             }
    //         }
    //     }
    // }

    /**
     * 获取子节点
     * @param  string  $path
     * @return mixed
     */
    public function getChildren($path){
        return $this->_zookeeper->getChildren($this->_currentPath . $path);
    }

    /**
     * 获得缓存文件路径
     * @param  void
     * @return string
     */
    private function _getConfigCacheFilePath(){
        $arr = explode('/', trim($this->_currentPath, '/'));
        $cacheConfigSign = implode(self::CONFIG_CACHE_FILE_NAME_SEPARATE_SIGN, $arr);
        $cacheFileName = $cacheConfigSign . '.php';
        return self::CONFIG_CACHE_DIR . $cacheFileName;
    }

    /**
     * 生成配置缓冲文件
     * @param  array   $config
     * @return void
     */
    public function generateConfigCache($config){
        $filePath = $this->_getConfigCacheFilePath();
        if(!is_file($filePath)){
            if(is_dir(self::CONFIG_CACHE_DIR)){
                if(!is_writeable(self::CONFIG_CACHE_DIR) || !chmod(self::CONFIG_CACHE_DIR, 0755)){
                    throw new \Exception('config cache path ' . self::CONFIG_CACHE_DIR . ' unwriteable');
                }
            }else{
                mkdir(self::CONFIG_CACHE_DIR, 0755, TRUE);
            }
        }
        // 以完整数组方式将配置写入文件
        $outputConfig = '<?php return ' . var_export($config, true) . ';'; 
        $fd = @fopen($filePath, 'w');
        if(is_resource($fd)){
            fwrite($fd, $outputConfig);
            fclose($fd);
        }
    }

    /**
     * 递归生成键值对
     * @param  array  $config
     * @return array
     */
    // private function _recursionGenerateKV($config, $key = ''){
    //     $arr = [];
    //     $kvConfig = [];
    //     foreach($config as $name => $value){
    //         $k = $key ? $key . ZOOKEEPER_GROUP_SEPARATE_SIGN . $name : $name;
    //         if(is_array($value)){
    //             $arr = $this->_recursionGenerateKV($value, $k);
    //         }else{
    //             $arr[$k] = $value;
    //         }
    //         $kvConfig = array_merge($kvConfig, $arr);
    //     }
    //     return $kvConfig;
    // }

    /**
     * 生成分组键值对
     * @param  array  $config
     * @param  int    $mode    模式（1-项目key下全部数据 2-单个zk分组数据）
     * @return array
     */
    public function generateGroupKV($config, $mode){
        $customGroupName = '';
        $groupKVConfig = [];
        switch($mode){
            // 项目key下全部数据
            case 1:
                foreach($config as $zkGourp => $value){
                    if(is_array($value)){
                        foreach($value as $key => $val){
                            $tmpArr = explode(self::CUSTOM_GROUP_SEPARATE_SIGN, $key);
                            // 存在自定义分组
                            if(isset($tmpArr[1])){
                                $groupKVConfig[$zkGourp][$tmpArr[0]][$tmpArr[1]] = $val;
                            }else{
                                $groupKVConfig[$zkGourp][self::CUSTOM_DEFAULT_GROUP_NAME][$tmpArr[0]] = $val;
                            }
                        }
                    }else{
                        throw new \Exception('zookeeper group ' . $zkGourp . ' config exception');
                    }
                }
            break;
            // 单个zk分组数据
            case 2:
                foreach($config as $key => $value){
                    $tmpArr = explode(self::CUSTOM_GROUP_SEPARATE_SIGN, $key);
                    // 存在自定义分组
                    if(isset($tmpArr[1])){
                        $groupKVConfig[$tmpArr[0]][$tmpArr[1]] = $value;
                    }else{
                        $groupKVConfig[$tmpArr[0]] = $value;
                    }
                }
            break;
            default:
                throw new \Exception('config_center function generateGroupKV param mode value invalid, mode: ' . $mode);
        }
        return $groupKVConfig;
    }

    /**
     * 递归获取配置
     * @param  string  $path
     * @param  bool    $watch
     * @return mixed
     */
    private function _recursionGetConfig($path, $watch = FALSE){
        $result = $this->_zookeeper->exists($path);
        // 节点不存在
        if(!$result){
            return FALSE;
        }
        $config = [];
        $result = $this->_zookeeper->getChildren($path);
        // 叶子节点通过添加watch监听
        if(empty($result)){
            $endArr = explode('/', $path);
            $endStr = end($endArr);
            if(!in_array($endStr, ['change', 'unchange'])){
                if($watch === FALSE){
                    $config = $this->_zookeeper->get($path);
                }else{
                    $config = $this->_zookeeper->watch($path, [$this, 'watcher']);
                }
            }
        // 非叶子节点继续递归
        }else{
            foreach($result as $key => $val){
                $nextPath = $path . '/' . $val;
                $config[$val] = $this->_recursionGetConfig($nextPath, $watch);
                $nextPath = '';
            }
        }
        return $config;
    }
}